[Java][Jackson][怪談] ObjectMapperのFormatが効かないDate
JavaのDate
こんにちは。小室です。
Javaで開発したことがある人にはよく御存知の通り、Javaには昔から日付Date型が二種類、java.sql.Dateとjava.util.Dateがあります。
まあ、世の中には意外と意識せずに使う人とかも多く、地雷を踏み抜いたり(踏み抜いたまま引き継いだり)した人も多いかと思います。あるある話の一つなのではないでしょうか。
自分も今回久しぶりに地雷を踏み抜いたので、自らの迂闊さへの反省とJavaの厄介なDateクラスへの若干の怒りとともにここに記します。
JacksonのObjectMapperで簡単Jsonオブジェクト変換
JacksonはJavaでの開発ではよく使われるJsonプロセッサの一つです。
データの入れ物T型を定義しておくだけで、簡単にJsonオブジェクトからT型、逆にT型からJsonオブジェクトの変換を行ってくれます。
その中で日付のフォーマットの扱いというのも非常に簡単にできます。
日時の表記というのは、それこそ千差万別、「yyyyMMdd」もあれば「yyyy-MM-dd HH:mm:ss」かもしれないし、はたまたTimezoneつきかもしれない。「yyyy/MM/dd」なんて可能性もありますね。
時と場合と地域によって様々です。
JacksonのObjectMapperは、DateFormatterを指定することができます。
そのため、どんな表記であっても「Date型からJsonオブジェクトの中の日付は指定のフォーマットの文字列へ」相互変換することが簡単にできると思っていたのです。
事故が起きるまでは
動作がおかしい??
「JacksonのObjectMapperを使っていて何の問題が発生したのか?」
それはどんなDateFormatterを設定しても全く変換されないDate型の変数があったのです。その変数は他の定義と同じくjava.util.Dateで指定しているはずなのに。
何故か一つだけ、かたくなにFormatterの指定をスルーし続けました。
public class CompareDate { /** java.util.Date */ public Date date1; /** java.util.Date */ public Date date2; public static CompareDate createDefault() { // 現在日時を設定する CompareDate result = new CompareDate(); long currentTimes = System.currentTimeMillis(); result.date1 = new Date(currentTimes); result.date2 = new java.sql.Date(currentTimes); return result; } }
まあ、ちょっと小細工が入ってますが、こんな入れ物を用意しておきます。
今回Jsonでは「yyyyMMdd」と変換してもらうようにObjectMapperを設定します。
ObjectMapper mapper = new ObjectMapper(); mapper.setDateFormat(new SimpleDateFormat("yyyyMMdd")); Json.setObjectMapper(mapper);
これでDateで表現された値は全て指定のFormatに変換されます。
CompareDate compareDate = CompareDate.createDefault(); Json.stringify(Json.toJson(sample);
結果を見てみます。
{"date1":"20141121","date2":"2014-11-21"}
同じjava.util.Dateのはずですが、同じ結果になりません。
犯人はこいつです。
result.date2 = new java.sql.Date(currentTimes);
解決編
今回のイントロで既にネタ晴らしされていますが、Javaの中にある2種類のDate型の罠でした。
一つずつ解説します。
java.sql.Dateとjava.util.Date
java.sql.Dateとjava.util.Dateの変換というのは、かなり昔から話題にされており、すでに昔話の域かもしれません。
厄介なのが、この二つのDateは似て非なるものです。ひがさんの記事を見てみると
時間、分、秒、ミリ秒をゼロに設定することで、「標準化」する必要があります。
これは、すなわち精度が異なるということになります。
java.sql.Dateはjava.util.Dateのサブクラス
java.sql.Dateは、java.util.Dateのサブクラスとして定義されています。
java.sql.Dateで変数が定義されていれば、どんなうかつなプログラマーでも警戒します。
しかし、java.util.Dateのサブクラスであるが故に、java.util.Dateで定義された変数へ代入することも出来てしまうのです。
java.sql.DateはObjectMapperのDateFormatの指定を拒む
今回初めてわかったことなのですが、JacksonのObjectMapperで指定したDateFormatterはjava.util.Dateにしか適用されません。そのサブクラスであるjava.sql.Dateは完全にスルーされます。
厄介なのが、変数として定義してあるのはjava.util.Dateなのに、代入されている値がjava.sql.Dateだと見た目同じに見えてしまう点です。
ObjectMapperを通した後の結果が変わりますが、元の代入元までさかのぼらないと原因が分かりません。
まとめ
今回は代入箇所がすぐ近くだったのですぐ気づきました。これが遠く、さらに自分以外の担当者が入れていたとしたらどうでしょうか?
気づく人は少ないのではないかと思います。
あなたがjava.util.Dateで宣言している値に入ってきてるのは、本当にjava.util.Dateの値ですか?
誰かが勝手にjava.sql.Dateの値を入力してたりしていませんか?
事故が起きてからでは遅いかもしれませんよ?
それでは皆様ごきげんよう